Skip to content

Refactor UI components, enhance markdown support, and improve accessibility#59

Open
aditya-2k23 wants to merge 29 commits intomainfrom
refactor/design
Open

Refactor UI components, enhance markdown support, and improve accessibility#59
aditya-2k23 wants to merge 29 commits intomainfrom
refactor/design

Conversation

@aditya-2k23
Copy link
Copy Markdown
Owner

@aditya-2k23 aditya-2k23 commented May 2, 2026

This pull request introduces a centralized design token system for theming, improves dark mode and accessibility, optimizes Firestore deletion, and updates several UI components for consistency and maintainability. The most important changes are summarized below.


Design System & Theming

  • Introduced a new app/design-tokens.css file containing all design tokens (colors, fonts, spacing, shadows, etc.) for consistent theming, and refactored app/globals.css to import and use these tokens. Removed legacy CSS variables from globals.css and replaced them with references to the new design tokens. Also added dark mode overrides and semantic tokens for better maintainability and theme switching. [1] [2] [3]
  • Updated component styles to use the new design tokens, including improved shadows, glows, and color handling for elements like .purpleShadow, .glow, .blob-btn, and markdown content. Added .smooth-transition utility and improved focus and contrast for .journal-textarea. [1] [2] [3] [4] [5] [6] [7] [8]

Firestore Deletion Optimization

  • Refactored the recursive deleteCollectionTree function in app/api/delete-account/route.js to use Firestore batch deletes (500 per batch) for improved performance and reliability, passing the db instance explicitly. [1] [2]

UI/UX & Accessibility Improvements

  • Improved cursor styles and accessibility for interactive elements, ensuring correct pointer, text, and disabled cursors across more input types and roles. [1] [2]
  • Enhanced contrast and color handling for markdown previews and message bubbles, especially in dark mode.
  • Updated the homepage background overlay for better opacity handling in light/dark modes.
  • Improved the footer and section headers for better theme consistency and clarity. [1] [2]

Component & Dependency Updates

  • Replaced the default react-hot-toast Toaster with a custom CustomToaster component for more controlled notifications. [1] [2]

Documentation Updates

  • Cleaned up project guidelines in AGENT.md, removing redundant or outdated restrictions and clarifying best practices. [1] [2]

Summary by CodeRabbit

  • New Features

    • Rich-text editor and formatting toolbar for journals; TipTap-powered chat input and message rendering with Markdown.
    • Custom animated toaster for notifications.
  • Improvements

    • Centralized design tokens and improved dark-mode styling; global UI and cursor refinements.
    • Updated splash, loaders, and various component UI/UX tweaks.
  • Bug Fixes

    • More reliable account deletion via batched processing; sanitized chat history previews.

aditya-2k23 and others added 18 commits April 30, 2026 19:23
…les across various components

Co-authored-by: Copilot <copilot@github.com>
…onment variables and improve SSR compatibility
…s.css, CustomToaster, Journal, and MessageBubble
…bal styles to utilize them

Co-authored-by: Copilot <copilot@github.com>
Updated background opacity settings in the main page and added a decorative grid background with mask effects to the Login component. These changes include specific dark mode adjustments to maintain visual consistency.
Updated the Content Security Policy in next.config.mjs to include loom.com in connect-src and frame-src directives to support Loom video integrations.
Refine custom cursor logic in globals.css for better UX feedback. Update Input component focus shadow intensity and color. Migrate Login component from FontAwesome to Lucide icons and adjust styling.
Add ARIA labels and focus-visible styles to improve accessibility. Update save validation in JournalModal to correctly handle clearing existing entries. Implement GSAP SplitText cleanup in the splash screen to prevent element duplication. Refine the cloud sync status icon and chat header release tag logic.
@aditya-2k23 aditya-2k23 requested a review from Copilot May 2, 2026 13:12
@vercel
Copy link
Copy Markdown

vercel Bot commented May 2, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
moody Ready Ready Preview, Comment, Open in v0 May 5, 2026 2:49pm

@netlify
Copy link
Copy Markdown

netlify Bot commented May 2, 2026

Deploy Preview for moody-adi ready!

Name Link
🔨 Latest commit 3f16432
🔍 Latest deploy log https://app.netlify.com/projects/moody-adi/deploys/69fa034c23a4e8000817c049
😎 Deploy Preview https://deploy-preview-59--moody-adi.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

📝 Walkthrough

Walkthrough

Centralizes design tokens and dark mode, introduces a TipTap-based rich text editor + formatting toolbar, rewires journal/chat inputs to use Markdown editors, adds a custom GSAP-animated Toaster, hardens Firebase init for SSR, and changes Firestore account deletion to batched recursive deletes.

Changes

Editor + Design System + Infra

Layer / File(s) Summary
Dependencies & Config
package.json, tailwind.config.js, next.config.mjs, lib/release.js
Bumped version to 3.1.0; added TipTap, markdown, and typography deps; enabled @tailwindcss/typography; allowed https://*.loom.com in CSP; version label now omits empty release tag.
Design Tokens
app/design-tokens.css, app/globals.css
Added centralized CSS custom properties and .dark overrides; globals import updated; moved many theme variables to token usage; added .smooth-transition and updated cursors, scrollbar, journal/markdown, and blob/button styles.
Rich Text Editor Core
components/RichTextEditor.js, components/StyleTools.js
New TipTap-based RichTextEditor (markdown export, value-sync, editable toggle, isVoiceInput flag) and client-side StyleTools toolbar with formatting commands.
Editor Integration (Journal/Modal/Chat Input)
components/Journal.js, components/JournalModal.js, components/chat/ChatInput.js, components/chat/ChatContainer.js
Replaced textareas with RichTextEditor / TipTap editor; wired onChange/onEditorCreated; adjusted autosave/validation logic and voice integration; ChatInput refactored to TipTap markdown editor; ChatContainer exposes onChatHasMessages.
UI Components & Styling
components/Input.js, components/ThemeToggle.js, components/GlowBackground.js, components/AIInsightsSection.js, components/ConditionalFooter.js, components/Loader.js, components/Logout.js, components/MessageBubble.js, components/MessageList.js
Refactored input styling to ring-based approach; ThemeToggle uses CSS vars; Glow backgrounds use color-mix with tokens; AIInsights header and tip state added; markdown rendering in message bubbles; loader icon replaced; various class and dark-mode tweaks.
Splashscreen & Dashboard
components/Splashscreen.js, components/Dashboard.js
Splashscreen refactored to GSAP + SplitText with lucide icons and deterministic icon layout; Dashboard enforces a 2.5s min splash (minTimeMet) and updated loading progression.
Custom Toaster & Layout Wiring
components/CustomToaster.js, app/layout.js
Added client CustomToaster with GSAP entry/exit animations; RootLayout now renders CustomToaster instead of plain Toaster.
Firebase & Deletion Infra
firebase.js, app/api/delete-account/route.js
Introduced getFirebaseApp() to reuse/init app safely during SSR/build; refactored deleteCollectionTree to use batched (PAGE_SIZE=500) recursive deletes via db.batch().
Chat API / Prompts / History
app/api/chat/route.js, app/actions/insights.js, app/api/chat/history/route.js
Rewrote system/demo prompts to include rich-text awareness; tightened JSON-array parsing; updated insights prompts to respect formatting cues; sanitized/truncated preview construction in history route.
Docs / Misc
AGENT.md, .gitignore
Removed two restrictive bullets from AGENT.md; added .gitgenie/** to .gitignore.

Sequence Diagram

sequenceDiagram
    participant User
    participant Journal
    participant RichTextEditor
    participant StyleTools
    participant Server
    participant Firebase
    participant Firestore

    User->>Journal: Open editor / type content
    Journal->>RichTextEditor: mount with value, isVoiceInput flag
    User->>StyleTools: Click formatting
    StyleTools->>RichTextEditor: editor.chain().focus().toggle...().run()
    RichTextEditor->>Journal: onChange -> emits Markdown
    Journal->>Server: Trigger autosave (debounced)
    Server->>Firebase: save request (via firebase app from getFirebaseApp)
    Firebase->>Firestore: write document
    Firestore-->>Firebase: ack
    Firebase-->>Server: success
    Server-->>Journal: saved -> update cloud status

    User->>Server: Request account deletion
    Server->>Firebase: deleteUserFirestoreData -> deleteCollectionTree(collectionRef, db)
    loop per page (up to 500)
        Firebase->>Firestore: fetch page of docs
        Firebase->>Firestore: batch.delete(...) & commit
        Firebase->>Firestore: recursively delete nested collections
    end
    Firestore-->>Server: deletion complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • aditya-2k23/moody#57: Related — also touches app/api/delete-account/route.js and Firestore recursive deletion logic.
  • aditya-2k23/moody#50: Related — modifies app/api/chat/route.js and chat/prompt handling.
  • aditya-2k23/moody#46: Related — overlaps on chat API, AI insights, and several frontend chat components.

Poem

🐰 In tokens bright and editor's hum,
I nibble lines where markups come.
GSAP toasts and Firebase care,
Batched deletions tidy there.
Hop, tiptap, style — the UI's springing fun!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: UI component refactoring, markdown support enhancement, and accessibility improvements across the codebase.
Description check ✅ Passed The PR description comprehensively covers design tokens, Firestore optimization, UI/UX improvements, component updates, and documentation changes with relevant links and context.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/design

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (7)
components/Input.js (1)

4-4: ⚡ Quick win

Use focus-visible + scoped transitions to keep interactions calmer and cheaper

transition-all + always-on focus: glow can feel busy and animates more properties than needed. Prefer focus-visible: and a scoped transition (transition-shadow / transition-colors) for better accessibility/perf balance.

Suggested minimal tweak
-      className="dark:bg-slate-900/50 dark:text-slate-200 w-full max-w-[400px] mx-auto px-4 py-2 sm:py-3 rounded-full outline-none duration-200 transition-all ring-1 ring-indigo-500/70 dark:ring-indigo-500/30 focus:ring-indigo-500 focus:shadow-[0_0_20px_rgba(99,102,241,0.4)] dark:focus:shadow-[0_0_24px_rgba(99,102,241,0.4)] placeholder:font-sans"
+      className="dark:bg-slate-900/50 dark:text-slate-200 w-full max-w-[400px] mx-auto px-4 py-2 sm:py-3 rounded-full outline-none duration-200 transition-shadow transition-colors ring-1 ring-indigo-500/70 dark:ring-indigo-500/30 focus-visible:ring-indigo-500 focus-visible:shadow-[0_0_16px_rgba(99,102,241,0.35)] dark:focus-visible:shadow-[0_0_20px_rgba(99,102,241,0.35)] placeholder:font-sans"

As per coding guidelines: “Maintain a calm, minimal, non-distracting UI design philosophy; avoid loud colors, aggressive animations” and “Ensure all UI components prioritize mobile usability, accessibility, and performance.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Input.js` at line 4, Update the input element's className in the
Input component to replace the broad "transition-all" and always-on "focus:"
variants with scoped transitions and focus-visible selectors: remove
"transition-all" and use "transition-shadow" and/or "transition-colors", and
change "focus:" prefixed utilities (e.g., "focus:ring-indigo-500"
"focus:shadow-[...]") to "focus-visible:" equivalents so only keyboard focus
triggers the glow; keep existing dark-mode classes and ring/shadow values but
switch their prefixes to "focus-visible" and ensure the placeholder and sizing
classes remain unchanged.
components/Journal.js (1)

229-232: 💤 Low value

Whitespace-only entries can now be saved.

The condition changed from !entry.trim() to !entry, which means whitespace-only entries (e.g., " ") are now truthy and will trigger auto-save. This appears intentional per the summary, but verify this is the desired behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Journal.js` around lines 229 - 232, The current check uses if
(!entry && !lastSavedEntryRef.current) which treats whitespace-only strings as
non-empty and allows auto-save; change the condition to explicitly reject
whitespace-only content by using entry.trim() (e.g., if (!entry ||
!entry.trim()) or if (!entry.trim() && !lastSavedEntryRef.current) depending on
intended logic) so whitespace-only entries are not saved—update the condition
that references entry, lastSavedEntryRef.current and setCloudStatus accordingly
(or add an explicit trim-based guard earlier) to prevent saving
blank/whitespace-only notes.
components/RichTextEditor.js (1)

66-76: 💤 Low value

Voice input may not display while editor is focused.

The isFocused guard prevents content sync when the user is actively in the editor. If a user speaks while their cursor is in the editor, the voice transcript won't appear until they click away. This is likely an acceptable tradeoff to prevent cursor jumping during typing, but worth documenting or testing the voice-while-focused flow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/RichTextEditor.js` around lines 66 - 76, The current focus guard
(checking editor.isFocused before calling editor.commands.setContent) blocks
voice-transcript updates when the cursor is focused; add a clear bypass for
external updates by introducing and checking an explicit flag (e.g.,
isVoiceInput or externalUpdate) alongside value/currentMarkdown: when
isVoiceInput is true, call editor.commands.setContent(value, false, {
preserveWhitespace: "full" }) regardless of editor.isFocused so voice input
appears immediately; update the component prop/state that supplies this flag and
ensure the logic around value/currentMarkdown, editor.isFocused, and
editor.commands.setContent references that flag to allow external updates while
preserving the existing focus-protection for manual typing.
app/globals.css (3)

592-603: 💤 Low value

Hardcoded colors could reference design tokens.

The journal-textarea uses hardcoded hex values (#0f1724, #071025, #e6eef8) that match the design tokens (--color-text, --color-bg). For consistency with the token-based theming approach, consider referencing the tokens directly.

♻️ Suggested refactor
 .journal-textarea {
-  background: var(--color-white);
-  color: `#0f1724`;
+  background: var(--color-bg);
+  color: var(--color-text);
   box-shadow: 0 6px 18px rgba(99, 102, 241, 0.06);
 }

 .dark .journal-textarea {
-  background: `#071025`;
-  /* richer dark background */
-  color: `#e6eef8`;
+  background: var(--color-bg);
+  color: var(--color-text);
   box-shadow: 0 6px 22px rgba(2, 6, 23, 0.6);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/globals.css` around lines 592 - 603, Replace the hardcoded hex colors in
the .journal-textarea and .dark .journal-textarea rules with the existing CSS
design tokens (e.g., --color-text, --color-bg, --color-white or whichever tokens
map to those values) so the styles use token references instead of literal
values; update the color, background and box-shadow color stops to reference
tokens (and, if necessary, use rgba() with var(--token) or CSS color-mix/opacity
tokens) inside the .journal-textarea and .dark .journal-textarea selectors to
ensure theming consistency.

681-696: 💤 Low value

Markdown content colors could also use design tokens.

Similar to the journal-textarea, these hardcoded values duplicate what's defined in design-tokens.css.

♻️ Suggested refactor
 .markdown-content p {
-  color: `#0f1724`;
+  color: var(--color-text);
 }

 .dark .markdown-content p {
-  color: `#e6eef8`;
+  color: var(--color-text);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/globals.css` around lines 681 - 696, Replace the hardcoded color values
in the selectors .markdown-content p, .dark .markdown-content p,
.markdown-content a, and .dark .markdown-content a with the corresponding design
token CSS custom properties from design-tokens.css (e.g., text and link color
tokens) so the rules reuse the central tokens instead of duplicating hex values;
update the four selectors to reference the appropriate --token names used
elsewhere (matching the journal-textarea usage) and remove the literal hex
codes.

59-65: 💤 Low value

Consider using design tokens for transition values.

The .smooth-transition utility hardcodes 220ms and the cubic-bezier easing, but these values are already defined as --duration-base and --ease-default in design-tokens.css.

♻️ Suggested refactor
 .smooth-transition {
   transition:
-    background-color 220ms cubic-bezier(0.2, 0.9, 0.2, 1),
-    color 220ms cubic-bezier(0.2, 0.9, 0.2, 1),
-    box-shadow 220ms cubic-bezier(0.2, 0.9, 0.2, 1),
-    transform 220ms cubic-bezier(0.2, 0.9, 0.2, 1);
+    background-color var(--duration-base) var(--ease-default),
+    color var(--duration-base) var(--ease-default),
+    box-shadow var(--duration-base) var(--ease-default),
+    transform var(--duration-base) var(--ease-default);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/globals.css` around lines 59 - 65, Replace the hardcoded timing and
easing in the .smooth-transition rule with the design token variables: use
var(--duration-base) in place of 220ms and var(--ease-default) in place of the
cubic-bezier; update each property declaration (background-color, color,
box-shadow, transform) to use var(--duration-base) and var(--ease-default) and
optionally include fallbacks like var(--duration-base, 220ms) and
var(--ease-default, cubic-bezier(0.2,0.9,0.2,1)) to preserve behavior if tokens
are missing.
components/CustomToaster.js (1)

13-24: ⚡ Quick win

Kill in-flight GSAP tweens before starting a new one.

If t.visible flips to false while the 0.5 s entry animation is still running, a second gsap.to starts on the same element and both tweens fight each other — the exit animation loses. Calling gsap.killTweensOf(elRef.current) at the top of the effect (and returning it as cleanup) prevents this.

♻️ Proposed fix
  useEffect(() => {
+   if (!elRef.current) return;
+   gsap.killTweensOf(elRef.current);
    if (t.visible) {
      gsap.fromTo(elRef.current,
        { y: -30, opacity: 0, scale: 0.9 },
        { y: 0, opacity: 1, scale: 1, duration: 0.5, ease: "back.out(1.7)" }
      );
    } else {
      gsap.to(elRef.current, {
        y: -20, opacity: 0, scale: 0.9, duration: 0.3, ease: "power2.in"
      });
    }
+   return () => { gsap.killTweensOf(elRef.current); };
  }, [t.visible]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/CustomToaster.js` around lines 13 - 24, The useEffect that
animates elRef based on t.visible should kill any in-flight GSAP tweens before
starting a new tween and also clean them up on unmount: at the top of the
useEffect call gsap.killTweensOf(elRef.current) before invoking gsap.fromTo or
gsap.to, and return a cleanup function that calls
gsap.killTweensOf(elRef.current) to ensure no competing animations remain;
update the effect that references useEffect, elRef, t.visible, and the
gsap.fromTo/gsap.to calls accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/AIInsightsSection.js`:
- Around line 126-127: Update the header conditional so it shows the generation
state when insights are being created: use the existing isLoading flag together
with insights to decide the text (e.g., if isLoading OR insights is non-empty
show the "Lumi's Thoughts"/generation heading, otherwise show "Let's talk about
your day"). Modify the JSX that renders Sparkles and the header (the line
currently using insights ? "Lumi's Thoughts" : "Let's talk about your day") to
check isLoading as well, so when InsightSkeletonLeft is active during first-time
generation the header matches the loading state.

In `@components/chat/ChatHeader.js`:
- Around line 59-61: The outer span in ChatHeader.js currently applies "hidden"
when APP_RELEASE_TAG is empty, which prevents the inner dot from ever showing;
make the behavior consistent by removing the conditional "hidden" from that
span's className and instead conditionally render the inner content based on
APP_RELEASE_TAG: keep the outer <span> (the badge span in the ChatHeader
component) always visible (no "hidden" class), and change the inner ternary so
it renders the APP_RELEASE_TAG when present and a visible dot element when
APP_RELEASE_TAG.length === 0; alternatively, if you prefer to hide the entire
badge when no tag exists, simplify the inner ternary to only render
APP_RELEASE_TAG and keep the outer className conditional — pick one of these two
consistent approaches and apply it to the badge span in ChatHeader.js.

In `@components/ConditionalFooter.js`:
- Line 20: The decorative star span (<span className="text-indigo-500
dark:text-indigo-400">✦</span>) is announced by screen readers; update that span
in ConditionalFooter.js to include aria-hidden="true" so the symbol is ignored
by assistive technologies while preserving visual appearance.

In `@components/CustomToaster.js`:
- Around line 62-70: The divider between the toast message and dismiss button is
missing because the wrapper div with className "flex border-slate-200
dark:border-slate-700 h-full" only sets border colors but not a border side;
update the wrapper div in CustomToaster.js (the div that contains the dismiss
<button> and X icon) to include a border side utility such as "border-l" (or
"border" if you want all sides) so the color classes take effect and the
separator renders in both light and dark themes.

---

Nitpick comments:
In `@app/globals.css`:
- Around line 592-603: Replace the hardcoded hex colors in the .journal-textarea
and .dark .journal-textarea rules with the existing CSS design tokens (e.g.,
--color-text, --color-bg, --color-white or whichever tokens map to those values)
so the styles use token references instead of literal values; update the color,
background and box-shadow color stops to reference tokens (and, if necessary,
use rgba() with var(--token) or CSS color-mix/opacity tokens) inside the
.journal-textarea and .dark .journal-textarea selectors to ensure theming
consistency.
- Around line 681-696: Replace the hardcoded color values in the selectors
.markdown-content p, .dark .markdown-content p, .markdown-content a, and .dark
.markdown-content a with the corresponding design token CSS custom properties
from design-tokens.css (e.g., text and link color tokens) so the rules reuse the
central tokens instead of duplicating hex values; update the four selectors to
reference the appropriate --token names used elsewhere (matching the
journal-textarea usage) and remove the literal hex codes.
- Around line 59-65: Replace the hardcoded timing and easing in the
.smooth-transition rule with the design token variables: use
var(--duration-base) in place of 220ms and var(--ease-default) in place of the
cubic-bezier; update each property declaration (background-color, color,
box-shadow, transform) to use var(--duration-base) and var(--ease-default) and
optionally include fallbacks like var(--duration-base, 220ms) and
var(--ease-default, cubic-bezier(0.2,0.9,0.2,1)) to preserve behavior if tokens
are missing.

In `@components/CustomToaster.js`:
- Around line 13-24: The useEffect that animates elRef based on t.visible should
kill any in-flight GSAP tweens before starting a new tween and also clean them
up on unmount: at the top of the useEffect call gsap.killTweensOf(elRef.current)
before invoking gsap.fromTo or gsap.to, and return a cleanup function that calls
gsap.killTweensOf(elRef.current) to ensure no competing animations remain;
update the effect that references useEffect, elRef, t.visible, and the
gsap.fromTo/gsap.to calls accordingly.

In `@components/Input.js`:
- Line 4: Update the input element's className in the Input component to replace
the broad "transition-all" and always-on "focus:" variants with scoped
transitions and focus-visible selectors: remove "transition-all" and use
"transition-shadow" and/or "transition-colors", and change "focus:" prefixed
utilities (e.g., "focus:ring-indigo-500" "focus:shadow-[...]") to
"focus-visible:" equivalents so only keyboard focus triggers the glow; keep
existing dark-mode classes and ring/shadow values but switch their prefixes to
"focus-visible" and ensure the placeholder and sizing classes remain unchanged.

In `@components/Journal.js`:
- Around line 229-232: The current check uses if (!entry &&
!lastSavedEntryRef.current) which treats whitespace-only strings as non-empty
and allows auto-save; change the condition to explicitly reject whitespace-only
content by using entry.trim() (e.g., if (!entry || !entry.trim()) or if
(!entry.trim() && !lastSavedEntryRef.current) depending on intended logic) so
whitespace-only entries are not saved—update the condition that references
entry, lastSavedEntryRef.current and setCloudStatus accordingly (or add an
explicit trim-based guard earlier) to prevent saving blank/whitespace-only
notes.

In `@components/RichTextEditor.js`:
- Around line 66-76: The current focus guard (checking editor.isFocused before
calling editor.commands.setContent) blocks voice-transcript updates when the
cursor is focused; add a clear bypass for external updates by introducing and
checking an explicit flag (e.g., isVoiceInput or externalUpdate) alongside
value/currentMarkdown: when isVoiceInput is true, call
editor.commands.setContent(value, false, { preserveWhitespace: "full" })
regardless of editor.isFocused so voice input appears immediately; update the
component prop/state that supplies this flag and ensure the logic around
value/currentMarkdown, editor.isFocused, and editor.commands.setContent
references that flag to allow external updates while preserving the existing
focus-protection for manual typing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c14a9520-bbd8-47ae-a694-62207ad15af5

📥 Commits

Reviewing files that changed from the base of the PR and between 809ffac and 60d6b87.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (32)
  • .gitignore
  • AGENT.md
  • app/api/delete-account/route.js
  • app/design-tokens.css
  • app/globals.css
  • app/layout.js
  • app/page.js
  • components/AIInsightsSection.js
  • components/ConditionalFooter.js
  • components/CustomToaster.js
  • components/Dashboard.js
  • components/GlowBackground.js
  • components/Input.js
  • components/Journal.js
  • components/JournalModal.js
  • components/Login.js
  • components/RichTextEditor.js
  • components/Splashscreen.js
  • components/StyleTools.js
  • components/ThemeToggle.js
  • components/chat/ChatHeader.js
  • components/chat/MessageBubble.js
  • components/chat/MessageList.js
  • components/landing/HeroSection.js
  • components/landing/LandingFooter.js
  • components/landing/LumiDemoSection.js
  • context/authContext.js
  • firebase.js
  • lib/release.js
  • next.config.mjs
  • package.json
  • tailwind.config.js
💤 Files with no reviewable changes (1)
  • AGENT.md

Comment thread components/AIInsightsSection.js Outdated
Comment thread components/chat/ChatHeader.js Outdated
Comment thread components/ConditionalFooter.js Outdated
Comment thread components/CustomToaster.js Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

aditya-2k23 and others added 4 commits May 5, 2026 16:41
aditya-2k23 and others added 7 commits May 5, 2026 17:57
…nalCTA, and HeroSection

Co-authored-by: Copilot <copilot@github.com>
…m display time

Co-authored-by: Copilot <copilot@github.com>
…kdown support, and custom scrollbar styles

Co-authored-by: Copilot <copilot@github.com>
…ance AIInsightsSection with chat message tracking
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/NavbarLinks.js (1)

132-175: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

createPortal is missing the required container argument (runtime blocker).

Line 132–175: createPortal is called with only the JSX children and no DOM container. This will fail at runtime when the mobile menu opens. The comment on line 131 indicates intent to portal to document.body, but the implementation is incomplete.

✅ Minimal fix
-        </div>
-      )}
+        </div>,
+        document.body
+      )}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/NavbarLinks.js` around lines 132 - 175, The createPortal call used
inside the mounted && isOpen block is missing the required container argument;
update the createPortal invocation that renders the mobile drawer (the block
that references mounted, isOpen, overlayRef, menuRef, linksRef, navLinks,
handleScroll, closeMenu, NewFeatureDot) to pass document.body as the second
argument (e.g., createPortal(<...>, document.body)); ensure this runs only on
the client (mounted guard already present) so the portal target is available.
🧹 Nitpick comments (4)
app/globals.css (1)

692-698: 💤 Low value

Redundant .dark .markdown-content p override.

Both selectors set color: var(--color-text);, and --color-text is already redefined under .dark in app/design-tokens.css. The dark rule is a no-op and can be removed.

♻️ Suggested change
 .markdown-content p {
   color: var(--color-text);
 }
-
-.dark .markdown-content p {
-  color: var(--color-text);
-}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/globals.css` around lines 692 - 698, Remove the redundant CSS rule ".dark
.markdown-content p" because it repeats "color: var(--color-text)" which is
already handled by redefining --color-text in .dark in app/design-tokens.css;
delete the entire ".dark .markdown-content p { color: var(--color-text); }"
block (leaving ".markdown-content p { color: var(--color-text); }" intact) and
run a quick style check to ensure no other rules rely on that specific selector
for p color.
app/api/chat/route.js (1)

331-339: 💤 Low value

Duplicate rich-text instructions in the system prompt.

The "RICH TEXT FORMATTING IN CHAT" block (Lines 331-339) and the "RICH TEXT AWARENESS" block (Lines 423-440) cover overlapping ground (same syntax list, same "if user formats something it matters" cue, same emphasis-with-bold guidance). The two sections can drift from each other and inflate token cost on every chat request without adding new semantics. Consider consolidating into a single section near the formatting guidance.

Also applies to: 421-440

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/chat/route.js` around lines 331 - 339, Duplicate rich-text guidance
appears twice in the system prompt as the "RICH TEXT FORMATTING IN CHAT" and
"RICH TEXT AWARENESS" blocks; remove one and consolidate both into a single
canonical section (merge the content, preserving the
bold/italic/blockquote/heading rules and the note about mirroring user
formatting) placed near the other formatting guidance in app/api/chat/route.js
so the semantics remain identical; update references or comments that pointed to
the removed block to the canonical block name to avoid drift.
components/chat/ChatContainer.js (1)

731-733: ⚡ Quick win

ReactMarkdown preview truncation can render broken markdown syntax.

session.preview is truncated server-side at character boundaries (first 40 chars of message content + '...') without considering markdown syntax boundaries. When truncation lands in the middle of a markdown token (e.g., **bold text truncated he...), ReactMarkdown renders the unclosed ** literally as raw characters, creating visual noise in the preview.

For preview text specifically, either render as plain text (removes the truncation concern) or ensure the parent container constraints still work well with the default <p> wrapper that ReactMarkdown outputs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/chat/ChatContainer.js` around lines 731 - 733, The preview uses
ReactMarkdown to render session.preview which is truncated server-side and can
leave unclosed markdown tokens; replace the ReactMarkdown rendering in
ChatContainer (where session.preview is used) with a plain-text render (e.g., a
p/div/span that outputs session.preview raw) so truncated markdown is not
interpreted, and ensure the surrounding container styling (truncate/wrapping)
still enforces the desired preview length/overflow.
components/chat/ChatInput.js (1)

29-53: ⚡ Quick win

Scope the GSAP animation to this toolbar instance.

Animating .style-tools-container by class will hit every matching element in the document. If another chat input or reused toolbar class is mounted, toggling this button can animate the wrong node too. A ref on the rendered toolbar keeps the animation local and avoids cross-instance bleed.

Also applies to: 160-179

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/chat/ChatInput.js` around lines 29 - 53, The GSAP calls in
toggleStyleTools are targeting the global ".style-tools-container" class and can
affect multiple instances; change the toolbar to use a React ref (e.g.,
styleToolsRef) attached to the rendered container and update toggleStyleTools to
animate styleToolsRef.current (use the DOM node) instead of the class selector,
preserving the same gsap.to / gsap.fromTo animation options and the
setShowStyleTools logic; ensure any other occurrences (the similar block noted
at lines 160-179) are updated the same way and include the ref in hook
dependencies if referenced.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/api/chat/history/route.js`:
- Around line 68-71: Replace the inconsistent greedy/one-char regexes with a
single helper function stripWrappingQuotes(text) that removes exactly one
matched pair of wrapping single or double quotes (e.g., using a regex that
captures a leading quote and ensures the trailing quote matches it) and use that
helper wherever the code currently trims on cleanContent/preview/assistant
content (references: the cleanContent variable, the assistant bubble content
generation, and the non-assistant/preview content path). Ensure all three sites
call stripWrappingQuotes(...) after trim() so quoting behavior is identical
across preview, assistant bubble, and other content paths.

In `@app/api/chat/route.js`:
- Line 281: The silent catch around the rollback call to
redis.decr(demoQuotaKey) removes error observability; replace the empty catch
with a minimal log (e.g., console.error or processLogger.error) that includes
the caught error and context (something like "Failed to rollback demo quota for
demoQuotaKey"), or if you intentionally want to ignore the error remove the
unused binding with catch { }—apply the same fix to the other occurrences of
redis.decr(demoQuotaKey) in this file so rollbacks remain observable and
consistent with the previous logging behavior.
- Around line 595-597: The current quote-stripping in the replyBubbles mapping
removes all leading/trailing quote characters and can corrupt legitimate
content; change the normalization to only remove a single matching pair of
surrounding quotes (or none) when the entire string is wrapped—e.g., trim the
item, then if it starts and ends with the same quote character (single or
double) remove one character from each end, otherwise leave it untouched; apply
the same fix to the analogous strip logic in the history-related code paths that
use the same replace pattern (the locations that perform
.replace(/^["']+|["']+$/g, "") for history parsing).

In `@app/globals.css`:
- Around line 30-53: There are duplicate .chat-scrollbar rules causing the later
block to override the earlier 4px/rgba styles; fix by consolidating into a
single canonical .chat-scrollbar declaration (and its .dark variant) instead of
keeping both: choose the desired width (4px vs 5px) and thumb styling, remove
the redundant block, or merge the new rgba rules into the existing block
(preserving any `@apply` classes) and ensure the .dark
.chat-scrollbar::-webkit-scrollbar-thumb hover state is included so only one set
of rules governs scrollbar width and colors.

In `@components/AIInsightsSection.js`:
- Around line 71-76: The component writes tip_used_${chatId} to localStorage but
never reads it and the useEffect that resets tipUsed when chatHasMessages is
false clobbers any persisted value; choose session-only behavior by removing the
dead persistence: delete the localStorage.setItem(...) call that writes
tip_used_${chatId}, and ensure only the component state tipUsed (managed via
setTipUsed and the existing useEffect that resets when chatHasMessages changes)
controls visibility; search for and remove any other references to the
tip_used_${chatId} key to avoid dangling persistence code.

In `@components/chat/ChatInput.js`:
- Around line 186-191: The EditorContent in ChatInput.js lacks an accessible
name; add one by providing an explicit aria-label (e.g., aria-label="Message
input") or aria-labelledby that references a visible label element so screen
readers get context; update the EditorContent element (props around editor,
onCompositionStart/onCompositionEnd) to include the aria attribute and, if using
aria-labelledby, add a corresponding visible <label> for the input to ensure
proper accessibility.
- Around line 56-71: Add a useEffect that watches the external input prop and,
when editor exists, compares the incoming input to the editor's current content
and only updates the editor when they differ; use the existing editor instance
(editor) and its read/command APIs to get current content and set content (e.g.,
read current content via editor.getHTML() / editor.getText() or the markdown
storage helper and call editor.commands.setContent(...) or the markdown set
command) to avoid echoing changes back into setInput; mirror the guard pattern
used for voice input so parent-driven clears/prefills replace the editor content
but editor-originated updates do not trigger redundant updates.

In `@components/Dashboard.js`:
- Around line 82-88: The minimum splash timeout in Dashboard (useEffect that
calls setMinTimeMet with 2500ms) is out of sync with the progress bar animation
in Splashscreen.js (4.2s) and the extra 500ms dismissal delay around the staged
progress block; consolidate into a single shared duration constant (e.g.
SPLASH_DURATION_MS = 4200) or pass that duration as a prop to Splashscreen,
replace the hardcoded 2500ms in the useEffect with the shared constant, and
remove or merge the extra 500ms wait in the staged progress/dismissal logic so
the splash unmounts only after the full shared duration and the bar completes.

In `@components/Loader.js`:
- Around line 3-6: The Loader component uses a runtime-interpolated Tailwind
class sm:text-${size} and also hardcodes text-4xl, so Tailwind will purge the
dynamic class and the size prop has no effect on mobile; replace the
interpolation by creating a static mapping (e.g., sizeClassMap) from allowed
size values ("base", "sm", "5xl") to concrete Tailwind classes and derive a
sizeClass variable (defaulting if missing) inside Loader, remove the hardcoded
text-4xl, and apply the resulting sizeClass and any responsive variant classes
(e.g., use separate mapped values for mobile and sm breakpoints or two mapped
keys like mobileSizeClass and smSizeClass) to LoaderCircle’s className so all
Tailwind classes are static and the size prop controls both mobile and sm
rendering.

In `@components/RichTextEditor.js`:
- Around line 47-50: The editor currently calls onUpdate -> onChange with
editor.storage.markdown.getMarkdown() and later compares value !==
currentMarkdown and uses editor.isFocused/isVoiceInput to decide setContent; to
fix flakiness: normalize both sides before comparing (e.g., collapse runs of
whitespace/newlines, trim, and normalize line endings) when comparing value and
currentMarkdown so harmless formatting diffs don't trigger setContent, and
additionally guard setContent to avoid clobbering selection by only forcing
setContent when !editor.isFocused || isVoiceInput is true (and ensure callers
set isVoiceInput before updating value) or introduce a short debounce/sequence
check before calling setContent; update logic in the onUpdate handler and the
value !== currentMarkdown check (referencing onUpdate,
editor.storage.markdown.getMarkdown, editor.isFocused, setContent, value,
currentMarkdown, isVoiceInput, onChange) accordingly.
- Around line 4-8: Remove the duplicate Underline extension import/registration:
delete the `Underline` import and any registration, or explicitly disable it in
`StarterKit` via `StarterKit.configure({ underline: false })` to avoid
duplicate-extension errors; then update the TipTap setContent call used on the
`editor` (currently `editor.commands.setContent(value, false, {
preserveWhitespace: "full" })`) to the v3 signature
`editor.commands.setContent(value, { emitUpdate: false, parseOptions: {
preserveWhitespace: "full" } })` so `preserveWhitespace` is honored during
voice-input syncs.

In `@components/Splashscreen.js`:
- Around line 10-22: The executable call gsap.registerPlugin(SplitText) appears
before the lucide-react import block, violating ES module rules; move the
gsap.registerPlugin(SplitText) line so that it occurs after all import
declarations (i.e., place it below the lucide-react imports or at the end of the
import section) ensuring SplitText is imported and registered only after all
imports are declared.

---

Outside diff comments:
In `@components/NavbarLinks.js`:
- Around line 132-175: The createPortal call used inside the mounted && isOpen
block is missing the required container argument; update the createPortal
invocation that renders the mobile drawer (the block that references mounted,
isOpen, overlayRef, menuRef, linksRef, navLinks, handleScroll, closeMenu,
NewFeatureDot) to pass document.body as the second argument (e.g.,
createPortal(<...>, document.body)); ensure this runs only on the client
(mounted guard already present) so the portal target is available.

---

Nitpick comments:
In `@app/api/chat/route.js`:
- Around line 331-339: Duplicate rich-text guidance appears twice in the system
prompt as the "RICH TEXT FORMATTING IN CHAT" and "RICH TEXT AWARENESS" blocks;
remove one and consolidate both into a single canonical section (merge the
content, preserving the bold/italic/blockquote/heading rules and the note about
mirroring user formatting) placed near the other formatting guidance in
app/api/chat/route.js so the semantics remain identical; update references or
comments that pointed to the removed block to the canonical block name to avoid
drift.

In `@app/globals.css`:
- Around line 692-698: Remove the redundant CSS rule ".dark .markdown-content p"
because it repeats "color: var(--color-text)" which is already handled by
redefining --color-text in .dark in app/design-tokens.css; delete the entire
".dark .markdown-content p { color: var(--color-text); }" block (leaving
".markdown-content p { color: var(--color-text); }" intact) and run a quick
style check to ensure no other rules rely on that specific selector for p color.

In `@components/chat/ChatContainer.js`:
- Around line 731-733: The preview uses ReactMarkdown to render session.preview
which is truncated server-side and can leave unclosed markdown tokens; replace
the ReactMarkdown rendering in ChatContainer (where session.preview is used)
with a plain-text render (e.g., a p/div/span that outputs session.preview raw)
so truncated markdown is not interpreted, and ensure the surrounding container
styling (truncate/wrapping) still enforces the desired preview length/overflow.

In `@components/chat/ChatInput.js`:
- Around line 29-53: The GSAP calls in toggleStyleTools are targeting the global
".style-tools-container" class and can affect multiple instances; change the
toolbar to use a React ref (e.g., styleToolsRef) attached to the rendered
container and update toggleStyleTools to animate styleToolsRef.current (use the
DOM node) instead of the class selector, preserving the same gsap.to /
gsap.fromTo animation options and the setShowStyleTools logic; ensure any other
occurrences (the similar block noted at lines 160-179) are updated the same way
and include the ref in hook dependencies if referenced.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7985db90-10ec-4907-b96f-d7d0f7e23691

📥 Commits

Reviewing files that changed from the base of the PR and between 60d6b87 and 3f16432.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (25)
  • app/actions/insights.js
  • app/api/chat/history/route.js
  • app/api/chat/route.js
  • app/globals.css
  • components/AIInsightsSection.js
  • components/ConditionalFooter.js
  • components/CustomToaster.js
  • components/Dashboard.js
  • components/Input.js
  • components/Journal.js
  • components/JournalModal.js
  • components/Loader.js
  • components/Logout.js
  • components/NavbarLinks.js
  • components/RichTextEditor.js
  • components/Splashscreen.js
  • components/StyleTools.js
  • components/chat/ChatContainer.js
  • components/chat/ChatHeader.js
  • components/chat/ChatInput.js
  • components/chat/MessageBubble.js
  • components/chat/MessageList.js
  • components/landing/FinalCTA.js
  • components/landing/HeroSection.js
  • package.json
✅ Files skipped from review due to trivial changes (2)
  • components/chat/MessageList.js
  • components/Input.js
🚧 Files skipped from review as they are similar to previous changes (6)
  • components/CustomToaster.js
  • components/chat/MessageBubble.js
  • components/ConditionalFooter.js
  • components/JournalModal.js
  • package.json
  • components/Journal.js

Comment on lines +68 to +71
let cleanContent = data.content || "";
if (typeof cleanContent === "string") {
cleanContent = cleanContent.trim().replace(/^["']+|["']+$/g, "");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Inconsistent quote-stripping regex between assistant bubbles and other paths.

Line 70 and Line 93 use /^["']+|["']+$/g (greedy — strips all leading/trailing quote chars), while Line 86 uses /^["']|["']$/g (strips only a single char on each end). For the same source content, the preview, assistant bubble content, and non-assistant content can therefore disagree on how much quoting is removed. Pick one regex and apply consistently — preferably a "single matched pair" pattern so legitimate dialogue quotes within messages aren't clobbered:

♻️ Suggested helper
+const stripWrappingQuotes = (str) => {
+  if (typeof str !== "string") return str;
+  const trimmed = str.trim();
+  const match = trimmed.match(/^(["'])([\s\S]*)\1$/);
+  return match ? match[2] : trimmed;
+};

…then call stripWrappingQuotes(...) at all three sites.

This issue is paired with the same concern raised in app/api/chat/route.js Lines 595-597.

Also applies to: 86-86, 91-94

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/chat/history/route.js` around lines 68 - 71, Replace the inconsistent
greedy/one-char regexes with a single helper function stripWrappingQuotes(text)
that removes exactly one matched pair of wrapping single or double quotes (e.g.,
using a regex that captures a leading quote and ensures the trailing quote
matches it) and use that helper wherever the code currently trims on
cleanContent/preview/assistant content (references: the cleanContent variable,
the assistant bubble content generation, and the non-assistant/preview content
path). Ensure all three sites call stripWrappingQuotes(...) after trim() so
quoting behavior is identical across preview, assistant bubble, and other
content paths.

Comment thread app/api/chat/route.js
console.error("[Chat API] GEMINI_API_KEY is missing");
if (demoReserved && demoQuotaKey) {
try { await redis.decr(demoQuotaKey); } catch(e) {}
try { await redis.decr(demoQuotaKey); } catch (e) { }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Silently swallowed rollback errors lose observability.

These redis.decr rollbacks were previously logged; the new catch (e) { } form drops the failure silently and leaves e unused. If Redis is failing, the demo quota counter can drift upward without any signal in logs. A one-liner console.error (or at least catch { } to drop the unused binding cleanly) keeps signals consistent with the rollback log at Line 236.

🛠️ Suggested change
-        try { await redis.decr(demoQuotaKey); } catch (e) { }
+        try { await redis.decr(demoQuotaKey); } catch (rollbackError) {
+          console.error("[Chat API] Demo quota rollback failed", rollbackError);
+        }

Based on learnings: "Do not remove existing behavior silently" — the previous logging behavior shouldn't be dropped without intent.

Also applies to: 536-536, 674-674

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/chat/route.js` at line 281, The silent catch around the rollback call
to redis.decr(demoQuotaKey) removes error observability; replace the empty catch
with a minimal log (e.g., console.error or processLogger.error) that includes
the caught error and context (something like "Failed to rollback demo quota for
demoQuotaKey"), or if you intentionally want to ignore the error remove the
unused binding with catch { }—apply the same fix to the other occurrences of
redis.decr(demoQuotaKey) in this file so rollbacks remain observable and
consistent with the previous logging behavior.

Comment thread app/api/chat/route.js
Comment on lines 595 to 597
replyBubbles = parsed
.map((item) => (typeof item === "string" ? item.trim() : ""))
.map((item) => (typeof item === "string" ? item.trim().replace(/^["']+|["']+$/g, "") : ""))
.filter(Boolean);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Quote-stripping regex is greedy and can mutate legitimate content.

item.trim().replace(/^["']+|["']+$/g, "") strips all leading/trailing single and double quote characters. A legitimate model bubble such as "that one conversation that changed things" (intentional quoted phrase) or a closing dialogue line like she said "okay" will lose its quotes after this normalization. Since the model is already instructed to return a JSON array (so JSON-parsed strings are inherently unquoted), this regex mostly defends against a rare case — wrapping strings in extra escaped quotes — at the cost of clobbering deliberate quote characters.

Consider stripping at most one matched pair, or only when the entire string is wrapped in matching quotes:

♻️ Suggested change
-        .map((item) => (typeof item === "string" ? item.trim().replace(/^["']+|["']+$/g, "") : ""))
+        .map((item) => {
+          if (typeof item !== "string") return "";
+          const trimmed = item.trim();
+          // Only strip a single matched pair of wrapping quotes.
+          const match = trimmed.match(/^(["'])([\s\S]*)\1$/);
+          return match ? match[2] : trimmed;
+        })

The same concern applies to the analogous strip in app/api/chat/history/route.js at Lines 70, 86, and 93.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
replyBubbles = parsed
.map((item) => (typeof item === "string" ? item.trim() : ""))
.map((item) => (typeof item === "string" ? item.trim().replace(/^["']+|["']+$/g, "") : ""))
.filter(Boolean);
replyBubbles = parsed
.map((item) => {
if (typeof item !== "string") return "";
const trimmed = item.trim();
// Only strip a single matched pair of wrapping quotes.
const match = trimmed.match(/^(["'])([\s\S]*)\1$/);
return match ? match[2] : trimmed;
})
.filter(Boolean);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/chat/route.js` around lines 595 - 597, The current quote-stripping in
the replyBubbles mapping removes all leading/trailing quote characters and can
corrupt legitimate content; change the normalization to only remove a single
matching pair of surrounding quotes (or none) when the entire string is
wrapped—e.g., trim the item, then if it starts and ends with the same quote
character (single or double) remove one character from each end, otherwise leave
it untouched; apply the same fix to the analogous strip logic in the
history-related code paths that use the same replace pattern (the locations that
perform .replace(/^["']+|["']+$/g, "") for history parsing).

Comment thread app/globals.css
Comment on lines +30 to +53
.chat-scrollbar::-webkit-scrollbar {
width: 4px;
}

.chat-scrollbar::-webkit-scrollbar-track {
background: transparent;
}

.chat-scrollbar::-webkit-scrollbar-thumb {
background: rgba(199, 210, 254, 0.5);
border-radius: 4px;
}

.chat-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(199, 210, 254, 0.8);
}

.dark .chat-scrollbar::-webkit-scrollbar-thumb {
background: rgba(59, 66, 103, 0.5);
}

.dark .chat-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(59, 66, 103, 0.8);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Duplicate .chat-scrollbar rules — the newer block (Lines 30-53) is overridden.

There are now two complete .chat-scrollbar scrollbar declarations:

  • The new block at Lines 30-53 sets width: 4px and uses rgba(199, 210, 254, 0.5) thumb.
  • The existing block at Lines 957-979 sets width: 5px and uses @apply bg-indigo-200/60 ....

Because of cascade order (and equal specificity), the second block wins, so the new 4px/rgba styles never apply at runtime. Either delete the old block or remove the new one to avoid silent dead CSS and future drift.

Also applies to: 957-979

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/globals.css` around lines 30 - 53, There are duplicate .chat-scrollbar
rules causing the later block to override the earlier 4px/rgba styles; fix by
consolidating into a single canonical .chat-scrollbar declaration (and its .dark
variant) instead of keeping both: choose the desired width (4px vs 5px) and
thumb styling, remove the redundant block, or merge the new rgba rules into the
existing block (preserving any `@apply` classes) and ensure the .dark
.chat-scrollbar::-webkit-scrollbar-thumb hover state is included so only one set
of rules governs scrollbar width and colors.

Comment on lines +71 to +76
// Reset tip visibility when a new chat starts (chatHasMessages becomes false)
useEffect(() => {
if (!chatHasMessages) {
setTipUsed(false);
}
}, [chatHasMessages]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

tipUsed persistence is inconsistent — localStorage is written but never read.

Two concerns about the new tipUsed logic:

  1. Dead write to localStorage. Line 128 writes tip_used_${chatId} to localStorage, but nothing in this file (or, based on the diff context, elsewhere) ever reads that key. So the persisted flag has no effect on subsequent mounts — the user will see the "Click to chat with Lumi about this →" hint again on a hard refresh even after clicking it.

  2. Reset effect undermines any future persistence. The effect at Lines 72-76 unconditionally resets tipUsed to false whenever chatHasMessages becomes false (which is the initial state on every mount). So even if you wired up a localStorage.getItem read on mount, this effect would clobber it as soon as ChatContainer reports zero messages on first render.

Decide which behavior you actually want and align both pieces:

  • Persistent across sessions — read the flag once on mount/chatId change and gate the reset by that flag.
  • Session-only — drop the localStorage.setItem call entirely; component state alone is sufficient.
🛠️ Sketch (session-only — simplest)
-    setTipUsed(true);
-    if (typeof window !== "undefined" && chatId) {
-      localStorage.setItem(`tip_used_${chatId}`, "true");
-    }
+    setTipUsed(true);

Also applies to: 123-130

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/AIInsightsSection.js` around lines 71 - 76, The component writes
tip_used_${chatId} to localStorage but never reads it and the useEffect that
resets tipUsed when chatHasMessages is false clobbers any persisted value;
choose session-only behavior by removing the dead persistence: delete the
localStorage.setItem(...) call that writes tip_used_${chatId}, and ensure only
the component state tipUsed (managed via setTipUsed and the existing useEffect
that resets when chatHasMessages changes) controls visibility; search for and
remove any other references to the tip_used_${chatId} key to avoid dangling
persistence code.

Comment thread components/Dashboard.js
Comment on lines +82 to +88
// Hardcoded 2.5s minimum splash screen time
useEffect(() => {
const splashTimer = setTimeout(() => {
setMinTimeMet(true);
}, 2500);
return () => clearTimeout(splashTimer);
}, []);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

The splash lifetime no longer matches the progress animation.

Line 86 starts a 2500ms gate and Lines 200-202 add another 500ms before dismissal, but components/Splashscreen.js now animates the bar for 4.2s. That means the splash can unmount around 3.0s while the progress bar is still mid-animation, so users may never see a completed state. Keep this on a single shared duration, or drive the bar from the dashboard’s staged progress again. Based on learnings: When fixing bugs, explain root cause, fix minimal surface area, and avoid cascading refactors.

Also applies to: 196-205

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/Dashboard.js` around lines 82 - 88, The minimum splash timeout in
Dashboard (useEffect that calls setMinTimeMet with 2500ms) is out of sync with
the progress bar animation in Splashscreen.js (4.2s) and the extra 500ms
dismissal delay around the staged progress block; consolidate into a single
shared duration constant (e.g. SPLASH_DURATION_MS = 4200) or pass that duration
as a prop to Splashscreen, replace the hardcoded 2500ms in the useEffect with
the shared constant, and remove or merge the extra 500ms wait in the staged
progress/dismissal logic so the splash unmounts only after the full shared
duration and the bar completes.

Comment thread components/Loader.js
Comment on lines 3 to +6
export default function Loader({ size = '5xl' }) {
return (
<div className='flex flex-col flex-1 justify-center items-center gap-4'>
<i className={`fas fa-spinner animate-spin text-4xl text-indigo-600 sm:text-${size}`}></i>
<LoaderCircle className={`animate-spin text-4xl text-indigo-600 dark:text-indigo-400 sm:text-${size}`} />
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
printf 'Dynamic Tailwind interpolation in components/Loader.js:\n'
rg -n 'sm:text-\$\{size\}' components/Loader.js

printf '\nTailwind config safelist/pattern entries:\n'
fd -i 'tailwind.config.*' -x rg -n 'safelist|pattern|text-(sm|base|5xl)|sm:text-(sm|base|5xl)' {}

printf '\nLiteral size classes already present in source:\n'
rg -n --glob '!**/node_modules/**' '\btext-(sm|base|5xl)\b|\bsm:text-(sm|base|5xl)\b' .

Repository: aditya-2k23/moody

Length of output: 20238


Replace the runtime Tailwind class interpolation in the className prop.

Line 6 uses sm:text-${size} at runtime, which Tailwind cannot statically extract during the build process. This class will be purged in production. Additionally, the hardcoded text-4xl on the element means the size prop never affects mobile—the spinner always renders large regardless of the passed value ("base", "sm", or "5xl").

Use a static size-to-class mapping instead:

🔧 Minimal fix
+const SIZE_CLASS = {
+  sm: "text-sm",
+  base: "text-base",
+  "5xl": "text-5xl",
+};

 export default function Loader({ size = '5xl' }) {
   return (
     <div className='flex flex-col flex-1 justify-center items-center gap-4'>
-      <LoaderCircle className={`animate-spin text-4xl text-indigo-600 dark:text-indigo-400 sm:text-${size}`} />
+      <LoaderCircle className={`animate-spin text-indigo-600 dark:text-indigo-400 ${SIZE_CLASS[size] ?? SIZE_CLASS["5xl"]}`} />
     </div>
   )
 }

This ensures the classes are statically defined and responsive behavior works as intended.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/Loader.js` around lines 3 - 6, The Loader component uses a
runtime-interpolated Tailwind class sm:text-${size} and also hardcodes text-4xl,
so Tailwind will purge the dynamic class and the size prop has no effect on
mobile; replace the interpolation by creating a static mapping (e.g.,
sizeClassMap) from allowed size values ("base", "sm", "5xl") to concrete
Tailwind classes and derive a sizeClass variable (defaulting if missing) inside
Loader, remove the hardcoded text-4xl, and apply the resulting sizeClass and any
responsive variant classes (e.g., use separate mapped values for mobile and sm
breakpoints or two mapped keys like mobileSizeClass and smSizeClass) to
LoaderCircle’s className so all Tailwind classes are static and the size prop
controls both mobile and sm rendering.

Comment on lines +4 to +8
import StarterKit from "@tiptap/starter-kit";
import Placeholder from "@tiptap/extension-placeholder";
import Typography from "@tiptap/extension-typography";
import Underline from "@tiptap/extension-underline";
import { Markdown } from "tiptap-markdown";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm tiptap version and whether Underline is bundled with StarterKit.
fd -t f -g 'package.json' --exclude node_modules | xargs grep -nE '"@tiptap/(react|starter-kit|extension-underline)"|"tiptap-markdown"'

Repository: aditya-2k23/moody

Length of output: 221


🌐 Web query:

TipTap v3 StarterKit included extensions Underline

💡 Result:

Yes. In TipTap v3, @tiptap/starter-kit includes the Underline mark extension by default. Evidence: - The TipTap Editor Docs for StarterKit list Underline as “Included extensions” (marked as “Underline (New in v3)”). [1] - The StarterKit source code conditionally adds Underline.configure(...) when this.options.underline is not false. [2] - The TipTap Editor Docs for Underline describe how the Underline mark renders/normalizes to HTML underlines, confirming the extension is a first-class mark that StarterKit can load. [3] Practical note: you can disable it via StarterKit.configure({ underline: false }) (since StarterKit options gate whether Underline is pushed). [2]

Citations:


🌐 Web query:

TipTap v3 editor commands setContent signature options object emitUpdate

💡 Result:

TipTap v3 editor.commands.setContent signature and the meanings/defaults of its options object are: 1) Signature (TypeScript) setContent(content: Content | Fragment | ProseMirrorNode, options?: SetContentOptions): ReturnType [1] 2) SetContentOptions fields - parseOptions?: ParseOptions (defaults to {}) [1] - errorOnInvalidContent?: boolean (whether to throw if content is invalid) [1] - emitUpdate?: boolean (default true) - docs: “Whether to emit an update event. Defaults to true (Note: This changed from false in v2).” [2] - implementation: emitUpdate = true [1] 3) How emitUpdate is applied internally In the command implementation, TipTap sets a ProseMirror transaction meta flag to prevent the update when emitUpdate is false: - When dispatching, it uses tr.setMeta('preventUpdate', !emitUpdate) [1] - That means: emitUpdate: false => preventUpdate: true [1] 4) How this relates to setContent “commands setContent signature options object emitUpdate” - You can call it with just content: editor.commands.setContent('<p>Example Text</p>') [2] - Or with options to control parsing/validation and update emission: editor.commands.setContent('<p>Example Text</p>', { emitUpdate: false, parseOptions: { preserveWhitespace: 'full' }, errorOnInvalidContent: true }) [2] So, the key option you asked for is exactly emitUpdate (in the options object), and in TipTap v3 it defaults to true. [1][2]

Citations:


Fix TipTap v3 compatibility issues: remove duplicate Underline extension and correct setContent signature.

Two confirmed issues in this TipTap v3 editor:

  1. Duplicate Underline extension. StarterKit v3 includes Underline by default. Importing and registering it separately will cause a duplicate-extension error at editor initialization. Remove the Underline import and extension registration, or disable it in StarterKit config with StarterKit.configure({ underline: false }).

  2. Incorrect setContent signature. Line 75–77 uses the v2 API: editor.commands.setContent(value, false, { preserveWhitespace: "full" }). In v3, the signature is setContent(content, options) with a single options object. Change to: editor.commands.setContent(value, { emitUpdate: false, parseOptions: { preserveWhitespace: "full" } }). Without this fix, preserveWhitespace is ignored and voice-input syncs will not preserve formatting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/RichTextEditor.js` around lines 4 - 8, Remove the duplicate
Underline extension import/registration: delete the `Underline` import and any
registration, or explicitly disable it in `StarterKit` via
`StarterKit.configure({ underline: false })` to avoid duplicate-extension
errors; then update the TipTap setContent call used on the `editor` (currently
`editor.commands.setContent(value, false, { preserveWhitespace: "full" })`) to
the v3 signature `editor.commands.setContent(value, { emitUpdate: false,
parseOptions: { preserveWhitespace: "full" } })` so `preserveWhitespace` is
honored during voice-input syncs.

Comment on lines +47 to +50
onUpdate: ({ editor }) => {
const markdown = editor.storage.markdown.getMarkdown();
onChange?.(markdown);
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

External-source detection via editor.isFocused is fragile for voice input.

The branching at Line 72-78 assumes manual typing always coincides with editor.isFocused === true, and external updates (voice transcript, server load) coincide with false or isVoiceInput === true. Two edge cases worth checking:

  • Voice input while focused. If a voice transcript arrives while the editor still has focus, setContent will fire and reset the cursor selection mid-typing — acceptable here only because isVoiceInput is explicitly passed by the caller. Confirm the journal/voice flow always sets isVoiceInput before the prop value changes (otherwise the first transcript chunk silently drops).
  • onUpdate → parent state → prop round-trip. Every onUpdate calls onChange?.(markdown); if the parent stores it back into value, the next render runs this effect. The guard value !== currentMarkdown should bail because they match — but tiptap-markdown's getMarkdown() round-tripping is not guaranteed to be a fixed point (e.g. whitespace/newline normalization). If they ever differ, this will call setContent on every keystroke and clobber the user's selection.

Recommend confirming with a manual typing test (rapid characters, undo, multiline) and considering a normalize step before the !== compare.

Also applies to: 69-79

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/RichTextEditor.js` around lines 47 - 50, The editor currently
calls onUpdate -> onChange with editor.storage.markdown.getMarkdown() and later
compares value !== currentMarkdown and uses editor.isFocused/isVoiceInput to
decide setContent; to fix flakiness: normalize both sides before comparing
(e.g., collapse runs of whitespace/newlines, trim, and normalize line endings)
when comparing value and currentMarkdown so harmless formatting diffs don't
trigger setContent, and additionally guard setContent to avoid clobbering
selection by only forcing setContent when !editor.isFocused || isVoiceInput is
true (and ensure callers set isVoiceInput before updating value) or introduce a
short debounce/sequence check before calling setContent; update logic in the
onUpdate handler and the value !== currentMarkdown check (referencing onUpdate,
editor.storage.markdown.getMarkdown, editor.isFocused, setContent, value,
currentMarkdown, isVoiceInput, onChange) accordingly.

Comment on lines +10 to +22
gsap.registerPlugin(SplitText);
import {
Sparkles,
Brain,
Cloud,
Sun,
Heart,
Zap,
Smile,
Info,
Stars,
CheckCircle
} from "lucide-react";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
node <<'NODE'
const fs = require('fs');
const lines = fs.readFileSync('components/Splashscreen.js', 'utf8').split(/\r?\n/);
let sawStatement = false;

for (let i = 0; i < lines.length; i++) {
  const t = lines[i].trim();
  if (!t || t.startsWith('//') || t.startsWith('/*') || t.startsWith('*')) continue;
  const isImport = t.startsWith('import ');
  if (sawStatement && isImport) {
    console.log(`Import after statement on line ${i + 1}: ${t}`);
  }
  if (!isImport) sawStatement = true;
}
NODE

Repository: aditya-2k23/moody

Length of output: 553


🏁 Script executed:

cat -n components/Splashscreen.js | head -30

Repository: aditya-2k23/moody

Length of output: 1009


Move gsap.registerPlugin(...) to after the lucide-react import block.

Line 10 contains an executable statement, but line 11 starts an import. ES modules require all import declarations before any executable code. This violates the module syntax and will cause a parse error.

Fix
 import gsap from "gsap";
 import { useGSAP } from "@gsap/react";
 import SplitText from "gsap/SplitText";
-gsap.registerPlugin(SplitText);
-
 import {
   Sparkles,
   Brain,
   Cloud,
   Sun,
   Heart,
   Zap,
   Smile,
   Info,
   Stars,
   CheckCircle
 } from "lucide-react";
+
+gsap.registerPlugin(SplitText);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
gsap.registerPlugin(SplitText);
import {
Sparkles,
Brain,
Cloud,
Sun,
Heart,
Zap,
Smile,
Info,
Stars,
CheckCircle
} from "lucide-react";
import {
Sparkles,
Brain,
Cloud,
Sun,
Heart,
Zap,
Smile,
Info,
Stars,
CheckCircle
} from "lucide-react";
gsap.registerPlugin(SplitText);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/Splashscreen.js` around lines 10 - 22, The executable call
gsap.registerPlugin(SplitText) appears before the lucide-react import block,
violating ES module rules; move the gsap.registerPlugin(SplitText) line so that
it occurs after all import declarations (i.e., place it below the lucide-react
imports or at the end of the import section) ensuring SplitText is imported and
registered only after all imports are declared.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants